import datetime
import json
from typing import List
import happybase
import geohash
import numpy
from enum import Enum
import sys
import numpy as np
import time

# ------ 常量，值统一大写 ------
class WaveStrategy(Enum):
    GET_AVE = 1  # 计算浪高代价策略：取未来十小时平均值
    GET_MAX = 2  # 计算浪高代价策略：取未来十小时最大值
    GET_MIN = 3  # 计算浪高代价策略：取未来十小时最小值

class APCPStrategy(Enum):
    GET_AVE = 1  # 计算降雨量代价策略：取未来十小时平均值
    GET_MAX = 2  # 计算降雨量代价策略：取未来十小时最大值
    GET_MIN = 3  # 计算降雨量代价策略：取未来十小时最小值

class WindStrategy(Enum):
    GET_AVE = 1  # 计算风速代价策略：取未来十小时平均值
    GET_MAX = 2  # 计算风速代价策略：取未来十小时最大值
    GET_MIN = 3  # 计算风速代价策略：取未来十小时最小值

PRECISION = 0.25  # 天气数据的精度总是0.25度
UNITES_PER_DEGREE = 4  # int,总是等于round(1/PRECISION)

# ------ 配置：从文件中读取的值会覆盖这些值，应以配置文件中的值为准，这里给出了一些参考值。 ------
class DValid:
    # 记录一些天气数据的极值，用于剔除无效的数据，我们认为一条数据超过这些极值的两倍时这条数据无效将忽略它。
    wave_swell = 40 * 2  # 浪高历史记录大约是40米。
    APCP = 1870 * 2  # 24小时降雨量世界记录1870
    wind_speed = 114 * 2 # 世界最大风速历史记录是113.05m/s

class Extreme:
    # 浪高上下限
    max_height_of_wave_swell = 1.50  # 超过这个浪高就代表不可通行，代价值设为inaccessible_cost_value（255）。
    min_height_of_wave_swell = 0.5   # 小于这个浪高时无额外代价，代价值为默认代价DEFAULT_COST_VALUE(1)。
    # 降雨上下限，可以认为中雨以下（apcp < 10mm）比较适合航行
    max_APCP = 10.0  # 超过这个降雨量认为不可航行。
    min_APCP = 5.0   # 低于这个降雨量认为无通行代价。
    # 风速上下限(m/s) wind
    max_wind = 10.7  # 超过这个风速时认为不可通航
    min_wind = 5.0  # 低于这个风速时认为没有额外代价。

class CostWeights:
    # 一个网格的通行代价可能由多个分量组成，需要根据各分量的值计算出最终的通行代价。你可以自由设置各分量的权重。
    # 通用公式是: 代价 = 浪高代价 * 浪高代价的权重 + 降雨量代价 * 降雨量代价的权重 + ...
    # 注意1：一个分量的权重可以为0（即不参与代价的计算），但不能是负数。
    # 注意2：各权重之和必须为1.0。
    wave = 0.3  # 总代价的值中浪高的权重
    APCP = 0.3  # 总代价的值中降雨量的权重
    wind = 0.4  # 总代价的值中风速的权重

class CommonConf:
    default_cost_value = 1  # 在网格初始化时默认填入的代价值
    no_cost_value = 1  # 在天气条件非常好，非常适合航行的情况下，代价值是多少。
    min_cost_value = 2  # 当有通行代价时，代价值开始的数值。
    max_cost_value = 127  # 当有通行代价时，代价值的上限。
    inaccessible_cost_value = 255  # 这个值表示代价过高不可通行
    # 最多读取多少个小时的天气预报信息。短距导航时可能只会在意未来几小时内的天气。
    # 注意！这个值最大能到385。120小时后不再连续预报每小时的天气信息，改成+3小时，所以[0,384]区间中会有空洞存在。
    longest_hours_of_weather_forecast = 10
    # 计算代价的策略，已知未来n小时的天气代价数组，怎么从中取得一个合适的代价值。
    cost_calc_strategy_4_wave = WaveStrategy.GET_MAX
    cost_calc_strategy_4_apcp = APCPStrategy.GET_MAX
    cost_calc_strategy_4_wind = WindStrategy.GET_MAX

# 当某个网格的浪高/降水/风速数据未来n小时缺失了应当怎么处理。true: 视为可通行(即对应分量设置为no_cost_value)，false: 视为不可通行。
class AccessibleWhenNoData:
    wave = True
    apcp = True
    wind = True

class WeatherReader:
    def __init__(self, config_file, host, port):
        """
        self.grid是用于存储天气代价的网格grid[x][y] = cost，每个格子是0.25度。经纬度-180, -90对应self.grid的原点
        如果按照寻路网格的0.01来存，假设每个格子是1字节，需要648,000,000字节。而按0.25只需要1,036,800字节。
        因为天气的精度是0.25，如果存0.01会有大量的重复数据。需要计算寻路网格i对应的坐标只需要整除就可以了Wi = Ri/25。
        hbase_host,hbase_port: 存放天气数据的hbase服务器IP和端口。
        wave_strategy: 计算浪高代价的策略，取未来十小时的最大值，最小值，平均值，见WaveStrategy。
        """
        self.hbase_host = host
        self.hbase_port = port
        self.__load_config_from_file(config_file)
        # 用numpy.ones将所有网格的代价都默认设置成1。在目前的A*寻路算法中，通过网格的代价默认是1。
        # 不能设置成0否则会导致总是优先通过这个节点从而导致寻路错误。
        self.grid = numpy.full((360 * UNITES_PER_DEGREE, 180 * UNITES_PER_DEGREE), CommonConf.default_cost_value, dtype=numpy.uint8)

        # 给浪高/降水量/风速分级，计算cost每加1时对应浪高要加多少。在初始化的时候算一次即可避免在后面每次拿到数据都要计算。
        self.height_per_cost = (Extreme.max_height_of_wave_swell - Extreme.min_height_of_wave_swell) / (CommonConf.max_cost_value - CommonConf.min_cost_value)
        self.apcp_mm_per_cost = (Extreme.max_APCP - Extreme.min_APCP) / (CommonConf.max_cost_value - CommonConf.min_cost_value)  # 同上，降水数据。
        self.win_mips_per_cost = (Extreme.max_wind - Extreme.min_wind) / (CommonConf.max_cost_value - CommonConf.min_cost_value)  # 同上，风速数据。

        # 按照权重计算代价值，数组的值加起来必须等于1，依次为浪高权重、降雨量权重。
        # 例如[0.6, 0.4] 表示最终的代价中浪高占60%，降雨量占40%
        if abs(CostWeights.wave + CostWeights.APCP + CostWeights.wind - 1.0) > 1e-6:
            raise Exception("因权重设置错误，程序不知道如何继续。")
        self.table_name = "sea_weather"

    def set_hbase_table(self, table_name):
        self.table_name = table_name

    def __load_config_from_file(self, file_path):
        with open(file_path, encoding='utf-8') as f:
            # 打开文件剔除#号开头的注释行，有注释的配置文件好管理。
            n = f.readlines()
            real_lines = []
            for i in n:
                x = i.strip()
                if x == "" or x[0] == "#":
                    continue
                real_lines.append(i)
            text = "\n".join(real_lines)
            # 读取配置文件内容并填写到配置对象中。
            cfg = json.loads(text)
            DValid.wave_swell = int(cfg["DValid"]["wave_swell"])
            DValid.APCP = int(cfg["DValid"]["APCP"])
            DValid.wind_speed = int(cfg["DValid"]["wind_speed"])
            Extreme.max_height_of_wave_swell = round(float(cfg["Extreme"]["max_height_of_wave_swell"]), 2)
            Extreme.min_height_of_wave_swell = round(float(cfg["Extreme"]["min_height_of_wave_swell"]), 2)
            Extreme.max_APCP = round(float(cfg["Extreme"]["max_APCP"]), 2)
            Extreme.min_APCP = round(float(cfg["Extreme"]["min_APCP"]), 2)
            Extreme.max_wind = round(float(cfg["Extreme"]["max_wind"]), 2)
            Extreme.min_wind = round(float(cfg["Extreme"]["min_wind"]), 2)
            CostWeights.wave = round(float(cfg["CostWeights"]["wave"]), 2)
            CostWeights.APCP = round(float(cfg["CostWeights"]["APCP"]), 2)
            CostWeights.wind = round(float(cfg["CostWeights"]["wind"]), 2)
            CommonConf.default_cost_value = int(cfg["CommonConf"]["default_cost_value"])
            CommonConf.no_cost_value = int(cfg["CommonConf"]["default_cost_value"])
            CommonConf.min_cost_value = int(cfg["CommonConf"]["min_cost_value"])
            CommonConf.max_cost_value = int(cfg["CommonConf"]["max_cost_value"])
            CommonConf.inaccessible_cost_value = int(cfg["CommonConf"]["inaccessible_cost_value"])
            CommonConf.longest_hours_of_weather_forecast = int(cfg["CommonConf"]["longest_hours_of_weather_forecast"])
            strategy_waves = {"max": WaveStrategy.GET_MAX, 'min': WaveStrategy.GET_MIN, 'ave': WaveStrategy.GET_AVE}
            strategy_apcp = {"max": APCPStrategy.GET_MAX, 'min': APCPStrategy.GET_MIN, 'ave': APCPStrategy.GET_AVE}
            strategy_wind = {"max": WindStrategy.GET_MAX, 'min': WindStrategy.GET_MIN, 'ave': WindStrategy.GET_AVE}
            self.wave_strategy = strategy_waves[cfg["CommonConf"]["wave_strategy"]]
            self.apcp_strategy = strategy_apcp[cfg["CommonConf"]["apcp_strategy"]]
            self.wind_strategy = strategy_wind[cfg["CommonConf"]["wind_strategy"]]
            AccessibleWhenNoData.wave = cfg["AccessibleWhenNoData"]["wave"]
            AccessibleWhenNoData.apcp = cfg["AccessibleWhenNoData"]["apcp"]
            AccessibleWhenNoData.wind = cfg["AccessibleWhenNoData"]["wind"]
        f.close()

    def dump_config(self):
        # 输出参数信息
        print("<-- 天气代价的关键运行参数 -->")
        print("- 天气数据来自hbase {}:{}".format(self.hbase_host, self.hbase_port))
        print("- 天气网格大小:", self.grid.shape[0], self.grid.shape[1])
        print("- 浪高小于等于", Extreme.min_height_of_wave_swell, "(米)时视为可任意通行，浪高大于等于", Extreme.max_height_of_wave_swell, "时视为不可通行")
        print("- 浪高每加", round(self.height_per_cost, 6), "(米)，代价就加1，")
        print("- 计算浪高代价分量的策略：{}。 注:1-未来n小时的均值，2-最大值，3-最小值".format(self.wave_strategy.value))
        print("- 计算降水量代价分量的策略：{}。 注:1-未来n小时的均值，2-最大值，3-最小值".format(self.apcp_strategy.value))
        print("- 计算风速代价分量的策略：{}。 注:1-未来n小时的均值，2-最大值，3-最小值".format(self.wind_strategy.value))
        print("- 降雨量小于等于", Extreme.min_APCP, "(毫米)时视为可任意通行，降雨量大于等于", Extreme.max_APCP, "时视为不可通行")
        print("- 降雨量每加", round(self.apcp_mm_per_cost, 6), "(毫米), 代价就加1")
        print("- 风速小于等于", Extreme.min_wind, "(m/s)时视为可以任意通行，风速大于等于", Extreme.max_wind, "时视为不可通行")
        print("- 风速每加", round(self.win_mips_per_cost, 6), "(m/s), 代价就加1")
        print("- 权重：wave={},APCP={},wind{}".format(round(CostWeights.wave, 2), round(CostWeights.APCP, 2), round(CostWeights.wind, 2)))
        print("")

    def reset(self):
        self.grid = numpy.ones((360 * UNITES_PER_DEGREE, 180 * UNITES_PER_DEGREE), dtype=numpy.uint8)

    def __waves_height2cost(self, h_wave_swell) -> int:
        """
        h_wave_swell: 浪高。小于等于min_height_of_wave_swell时认为无额外通行代价，大于等于max_height_of_wave_swell时认为不可通行。
        当浪高在(min_height_of_wave_swell, max_height_of_wave_swell)之间时有额外的通行代价。
        返回值：正常返回一个代表代价的整形值，可以是[1~127]或者是inaccessible_cost_value(255)。如发生错误返回-1。
        """
        if not isinstance(h_wave_swell, float) or h_wave_swell < 0:
            # print("计算浪高代价分量：错误的参数", h_wave_swell, file=sys.stderr)
            return -1
        if h_wave_swell > DValid.wave_swell:
            # print("计算浪高代价分量失败：脏数据。", precipitation_rate, file=sys.stderr)
            return -1
        if h_wave_swell >= Extreme.max_height_of_wave_swell:
            cost_of_waves_and_swell = CommonConf.inaccessible_cost_value  # 不可通行
        elif h_wave_swell <= Extreme.min_height_of_wave_swell:
            cost_of_waves_and_swell = CommonConf.no_cost_value  # 随便跑
        else:
            # 在(min_height_of_wave_swell,max_height_of_wave_swell)区间要给min_cost_value(2)~max_cost_value(127)
            # 之间的代价值。
            # 已知cost每加1，对应浪高就加self.height_per_cost(米)。
            cost_of_waves_and_swell = CommonConf.min_cost_value + round((h_wave_swell - Extreme.min_height_of_wave_swell) / self.height_per_cost)
        return cost_of_waves_and_swell

    def __precipitation_cost(self, precipitation_rate: float) -> int:
        """
        precipitation_rate: 降雨量。
        返回值：正常返回一个代表代价的整形值，可以是[1~127]或者是inaccessible_cost_value(255)。如发生错误返回-1。
        """
        if not isinstance(precipitation_rate, float) or precipitation_rate < 0:
            # print("计算降雨量代价分量：错误的参数", precipitation_rate, file=sys.stderr)
            return -1
        if precipitation_rate > DValid.APCP:
            # print("计算降雨量代价分量失败：脏数据。", precipitation_rate, file=sys.stderr)
            return -1
        if precipitation_rate >= Extreme.max_APCP:
            return CommonConf.inaccessible_cost_value
        elif precipitation_rate <= Extreme.min_APCP:
            return CommonConf.no_cost_value
        else:
            return CommonConf.min_cost_value + round((precipitation_rate - Extreme.min_APCP) / self.apcp_mm_per_cost)

    def __wind_speed2cost(self, wind_speed: float) -> int:
        if not isinstance(wind_speed, float) or wind_speed < 0:
            # print("计算风速代价分量：错误的参数", wind_speed, file=sys.stderr)
            return -1
        if wind_speed > DValid.wind_speed:
            # print("计算风速代价分量失败：脏数据。", wind_speed, file=sys.stderr)
            return -1
        if wind_speed >= Extreme.max_wind:
            return CommonConf.inaccessible_cost_value
        elif wind_speed <= Extreme.min_wind:
            return CommonConf.no_cost_value
        else:
            return CommonConf.min_cost_value + round((wind_speed - Extreme.min_wind) / self.win_mips_per_cost)

    def __calc_cost_impl(self, waves, precipitations, wind):
        skip_wave, skip_apcp, skip_wind = False, False, False
        if len(waves) == 0:  # 浪高数据缺失
            if AccessibleWhenNoData.wave:
                skip_wave = True
            else:
                return CommonConf.inaccessible_cost_value
        if len(precipitations) == 0:  # 降雨量数据缺失
            if AccessibleWhenNoData.apcp:
                skip_apcp = True
            else:
                return CommonConf.inaccessible_cost_value
        if len(wind) == 0:
            if AccessibleWhenNoData.wind:  # 风速数据缺失
                skip_wind = True
            else:
                return CommonConf.inaccessible_cost_value

        # 根据不同的浪高代价策略，从发布之时未来十小时的浪高中取一个浪高代价。
        if skip_wave:
            wave_cost = CommonConf.no_cost_value
        else:
            if self.wave_strategy == WaveStrategy.GET_MAX:  # 计算浪高代价策略：取未来n小时平均值
                wave_cost = max(waves)
            elif self.wave_strategy == WaveStrategy.GET_MIN:  # 计算浪高代价策略：取未来n小时最小值
                wave_cost = min(waves)
            else:
                wave_cost = round(numpy.mean(waves))  # 计算浪高代价策略：取未来十小时均值

        if skip_apcp:
            precipitations_cost = CommonConf.no_cost_value
        else:
            if self.apcp_strategy == APCPStrategy.GET_MAX:
                precipitations_cost = max(precipitations)
            elif self.apcp_strategy == APCPStrategy.GET_MIN:
                precipitations_cost = min(precipitations)
            else:
                precipitations_cost = round(numpy.mean(precipitations))  # 取未来n小时均值

        if skip_wind:
            wind_cost = CommonConf.no_cost_value
        else:
            if self.wind_strategy == WindStrategy.GET_MAX:
                wind_cost = max(wind)
            elif self.wind_strategy == WindStrategy.GET_MIN:
                wind_cost = min(wind)
            else:
                wind_cost = round(numpy.mean(wind))  # 取未来n小时均值

        if (wave_cost == CommonConf.inaccessible_cost_value
                or precipitations_cost == CommonConf.inaccessible_cost_value
                or wind_cost == CommonConf.inaccessible_cost_value):
            return CommonConf.inaccessible_cost_value # 这个代价是最大权重，表示不可通行，后面不用计算权重了。

        # 计算加权平均并四舍五入
        final_cost = round(CostWeights.wave * wave_cost + CostWeights.APCP * precipitations_cost + CostWeights.wind * wind_cost)
        assert 1 <= final_cost <= 127  # 可约束输出范围在1到127之间 max(1, min(127, final_cost))
        # print("格子final_cost:", final_cost)
        return final_cost

    def __calculate_weather_cost(self, weather) -> int:
        """
        weather：最近的天气预报。每个天气预报包含了从发布时之后十个小时的天气。
        {
        b'fc:fc000':
            b'{
                "tmp":28.6863, "prate":0.0000, "apcp":9.999e+20, "tcdc":4.1000, "gust":2.3068,
                "ugrd":-1.8335, "vgrd":1.2287, "wind":2.2071, "wdir":123.8276, "pres":101047.8047, "htsgw":0.5100,
                "dirpw":93.2100, "swell":0.3700, "swdir":96.1100, "swper":8.9900, "sst":29.4214, "ssh":0.6579,
                "land":0.0000
            }',
        b'fc:fc001': ...
        b'fc:fc002': ...
        ...
        b'fc:fc384': ...
        }
        返回值：返回代价值，-1表示计算代价失败。
        """
        # 存放海浪分量代价的列表(htsgw: height of waves and swell)
        cost_of_waves_and_swell = []
        # 存放降雨量代价的列表
        # apcp: Total Accumulated Precipitation Rate。总累积降水量，表示某段时间内（如6小时、24小时）的降水总量。
        cost_of_apcp = []
        # 存放wind代价的列表
        cost_of_wind = []
        # test_data_list = []

        for i in range(0, CommonConf.longest_hours_of_weather_forecast):
            k = 'fc:fc{:0>3d}'.format(i).encode()
            # print(k)
            v = weather.get(k)
            if not v:
                continue
            data_weather_hour = json.loads(v)
            # (1)取海浪分量的代价
            height_wave_swell = data_weather_hour.get('htsgw')
            cost_w = self.__waves_height2cost(height_wave_swell)
            if cost_w != -1:
                cost_of_waves_and_swell.append(cost_w)
            # else:
            #     breakpoint()  # 无意义，在调试时用来下断点查看数据。
            # (2)取降雨量分量的代价
            precipitation_rate = data_weather_hour.get('apcp')
            # test_data_list.append(precipitation_rate)
            cost_p = self.__precipitation_cost(precipitation_rate)
            if cost_p != -1:
                cost_of_apcp.append(cost_p)
            # (3)取风速风量的代价
            wind_speed = data_weather_hour.get('wind')
            cost_wind = self.__wind_speed2cost(wind_speed)
            if cost_wind != -1:
                cost_of_wind.append(cost_wind)

        # print(test_data_list)
        # (3) 加权计算各分量的值，目前是等权重。
        # print("浪高代价分量：", cost_of_waves_and_swell)
        # print("降雨量代价分量：", cost_of_apcp)
        # print("风速代价分量", cost_of_wind)
        return self.__calc_cost_impl(cost_of_waves_and_swell, cost_of_apcp, cost_of_wind)

    @staticmethod
    def __padding(v, x):
        """把v对齐到x；x<0时向下对齐，x>0时向上对齐"""
        v = int(v * 100)
        x0 = abs(x)
        # 不需要对齐了
        if v % x0 == 0:
            return v
        v = v // x0 * x0
        if x > 0:
            v += x
        return v

    def read_weather_and_cache(self, lon, lat, width, height, buf_file="./weather.buf"):
        self.read_weather(lon, lat, width, height)
        np.save(buf_file, self.grid)
        return self.grid

    def read_weather(self, lon, lat, width, height):
        hbase_conn = happybase.Connection(host=self.hbase_host, port=self.hbase_port)
        table = hbase_conn.table(self.table_name)  # 00/06/12/18 6小时更新一次
        print("输入：", lon, lat, width, height)
        # <<< --- 这部分代码由怡聪实现 - begin --- >>>
        lon += 180
        lat += 90
        # lat0, lon0向下对齐到25
        lat0 = WeatherReader.__padding(lat, -25)
        lon0 = WeatherReader.__padding(lon, -25)
        # lat1, lon1向上对齐到25
        lat1 = WeatherReader.__padding(lat + height*0.01, 25)
        lon1 = WeatherReader.__padding(lon + width*0.01, 25)
        if lat1 > 18000:
            lat1 = 18000
        if lon1 > 36000:
            lon1 = 36000
        data_parse = {}
        # data_parse的格式为键: (lon,lat),值：{ b'fc:fc000': $天气数据, b'fc:fc000': $天气数据, ... , b'fc:fc009': $天气数据}
        # 其中经度lon是360度制。
        # 按行取（纬度）
        # TODO 补充读取跨日期变更线附近的数据时的代码。
        for t in range(lat0, lat1, 25):
            row_key_begin = "%05d%05d" % (t, lon0)
            row_key_end = "%05d%05d" % (t, lon1)
            # print("find data from {} to {}".format(row_key_begin, row_key_end))
            for r in table.scan(row_key_begin, row_key_end):
                rowkey = r[0].decode()
                lon360, lat180 = int(rowkey[5:]), int(rowkey[:5])
                # print(f"{r[0]}, ({lon360}, {lat180}), ({r[1]})")
                # stime = float(r[1][b'fc:stime'].decode())
                # utime = float(r[1][b'fc:utime'].decode())
                # print("数据开始时间:", time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(stime)))
                # print("数据更新时间:", time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(utime)))
                # for fcid in [i for i in range(120)] + [j for j in range(123, 385, 3)]:
                #     fctime = stime + fcid * 3600
                #     column = ("fc:fc%03d" % (fcid)).encode()
                #     if column in r[1]:
                #         print("    预测时间: ", time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(fctime)))
                #         print("    预测数据: ", r[1][column])
        # <<< --- 这部分代码由怡聪实现 - end --- >>>
                # 总共有(360 * 4, 180 * 4)个格子，需要根据经纬度来算属于哪个格子。
                # data_parse的格式为键: (lon,lat),值：{ b'fc:fc000': $天气数据, b'fc:fc000': $天气数据, ... , b'fc:fc009': $天气数据}
                # 其中经度lon是360度制, lat是180度制。
                # 转成“寻路算法网格”的坐标。经纬度-180, -90对应的是网格坐标平面的原点，这是精度为0.01度时的坐标，取整很重要。
                coord_x, coord_y = lon360, lat180
                # 下面的x,y是这是精度为0.25时的坐标。x = coord_x//25,y = coord_y//25
                x, y = round(coord_x / (100 / UNITES_PER_DEGREE)), round(coord_y / (100 / UNITES_PER_DEGREE))
                if r[1] is not None:
                    cost = self.__calculate_weather_cost(r[1])
                    # 注意这里：如计算代价失败则视为不可通行。目前发有连续丢失前n个小时的海浪数据导致数据丢失。
                    self.grid[x][y] = CommonConf.inaccessible_cost_value if cost == -1 else cost
                    # print(x, y, self.grid[x][y])
                else:
                    print(x, y, '无数据')

        print(datetime.datetime.now(), "数据读取完成")
        # 当需要拓展到0.01度的时候，可以像下面这样操作，把每个元素复制二十五份：
        # l = np.repeat(np.repeat(n, 25, axis=0), 25, axis=1)
        # 然后叠加到原代价阵列(假设是g)
        # g = np.clip(g + l, 0, 127)  # g + l
        return self.grid
